//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

import card from '@hcengineering/card'
import contact, {
  Channel,
  Contact,
  contactId,
  Employee,
  formatContactName,
  formatName,
  getFirstName,
  getLastName,
  getName,
  Organization,
  Person,
  PersonSpace,
  type UserProfile
} from '@hcengineering/contact'
import core, {
  AccountRole,
  AccountUuid,
  concatLink,
  Doc,
  Hierarchy,
  MarkupBlobRef,
  Ref,
  SocialIdType,
  type Space,
  SpaceType,
  systemAccountUuid,
  readOnlyGuestAccountUuid,
  Tx,
  TxCreateDoc,
  TxCUD,
  TxMixin,
  TxRemoveDoc,
  TxUpdateDoc,
  TypedSpace,
  TxFactory,
  PermissionsGrant
} from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { makeRank } from '@hcengineering/rank'
import { getAccountBySocialId, getCurrentPerson } from '@hcengineering/server-contact'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { workbenchId } from '@hcengineering/workbench'

export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
  const result: Tx[] = []
  for (const tx of txes) {
    const ctx = tx as TxUpdateDoc<SpaceType>
    const newMember = ctx.operations.$push?.members as AccountUuid
    if (newMember !== undefined) {
      const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId })
      for (const space of spaces) {
        if (space.members.includes(newMember)) continue
        const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
          $push: {
            members: newMember
          }
        })
        result.push(pushTx)
      }
    }
    const oldMember = ctx.operations.$pull?.members as AccountUuid
    if (ctx.operations.$pull?.members !== undefined) {
      const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId })
      for (const space of spaces) {
        if (!space.members.includes(oldMember)) continue
        const pullTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
          $pull: {
            members: oldMember
          }
        })
        result.push(pullTx)
      }
    }
  }
  return result
}

async function getGrantSpaces (control: TriggerControl, grant?: PermissionsGrant): Promise<Space[]> {
  const spacesRefs = grant?.spaces
  if (spacesRefs == null) {
    return []
  }

  const grantSpaces = await control.findAll(control.ctx, core.class.Space, { _id: { $in: spacesRefs } })
  const allowedGrantedSpaces: Space[] = []

  for (const space of grantSpaces) {
    if (!space.private) {
      allowedGrantedSpaces.push(space)
      continue
    }

    const grantedBy = grant?.grantedBy
    if (grantedBy == null) {
      control.ctx.warn('Granted private space access without grantor is not allowed', { space })
      continue
    }

    if (space.members.includes(grantedBy)) {
      allowedGrantedSpaces.push(space)
    } else {
      control.ctx.warn('Granted private space access but the grantor is not a member of the workspace', {
        space,
        grantedBy
      })
    }
  }

  return allowedGrantedSpaces
}

export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
  const result: Tx[] = []

  const systemTxFactory = new TxFactory(core.account.System, false)
  const systemTxes: Tx[] = []

  for (const tx of _txes) {
    const mixinTx = tx as TxMixin<Person, Employee>
    if (mixinTx.attributes.active !== true) continue

    const person = (await control.findAll(control.ctx, contact.class.Person, { _id: mixinTx.objectId }))[0]
    const account = person?.personUuid as AccountUuid
    if (account === undefined) continue

    const txes = await createPersonSpace(account, mixinTx.objectId, control)
    result.push(...txes)

    const emp = control.hierarchy.as(person, contact.mixin.Employee)
    if (emp.role === 'GUEST') {
      let readOnlyGuestSpaces: Space[] = []
      const readonlyEmployees = await control.findAll(control.ctx, contact.mixin.Employee, {
        personUuid: readOnlyGuestAccountUuid
      })
      if (readonlyEmployees.length !== 0) {
        const readonlyEmployee = readonlyEmployees[0]
        if (readonlyEmployee.active) {
          readOnlyGuestSpaces = await control.findAll(control.ctx, core.class.Space, {
            members: readOnlyGuestAccountUuid
          })
        }
      }

      const grantSpaces = await getGrantSpaces(control, control.ctx.contextData.grant)

      for (const space of [...readOnlyGuestSpaces, ...grantSpaces]) {
        if (space._class === contact.class.PersonSpace || space.members.includes(account)) continue

        const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
          $push: {
            members: account
          }
        })
        systemTxes.push(pushTx)
      }

      const collabs = await control.findAll(control.ctx, core.class.Collaborator, {
        collaborator: readOnlyGuestAccountUuid
      })

      for (const collab of collabs) {
        const pushTx = systemTxFactory.createTxCreateDoc(core.class.Collaborator, collab.space, {
          attachedTo: collab.attachedTo,
          collaborator: account,
          attachedToClass: collab.attachedToClass,
          collection: 'collaborators'
        })
        systemTxes.push(pushTx)
      }

      continue
    }

    const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
    const grantSpaces = await getGrantSpaces(control, control.ctx.contextData.grant)

    for (const space of [...spaces, ...grantSpaces]) {
      if (space.members.includes(account)) continue

      const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
        $push: {
          members: account
        }
      })
      systemTxes.push(pushTx)
    }
  }

  await control.apply(control.ctx, systemTxes)

  const account = control.ctx.contextData.account
  if (account.role !== AccountRole.Owner) return result

  const typedSpaces: Space[] = [
    ...(await control.findAll(control.ctx, core.class.TypedSpace, {})),
    ...(await control.findAll(control.ctx, card.class.CardSpace, { space: core.space.Space }))
  ]

  for (const space of typedSpaces) {
    if (space === undefined) continue

    const owners = space.owners ?? []

    if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) {
      result.push(
        control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
          owners: [account.uuid]
        })
      )
    }
  }

  return result
}

export async function OnTypedSpaceCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
  const result: Tx[] = []
  for (const tx of _txes) {
    const ctx = tx as TxCreateDoc<TypedSpace>
    const owners = ctx.attributes.owners ?? []

    if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) {
      const members = ctx.attributes.members
      if (members.length === 0) continue
      result.push(
        control.txFactory.createTxUpdateDoc(ctx.objectClass, ctx.space, ctx.objectId, {
          owners: [members[0]]
        })
      )
    }
  }
  return result
}

export async function OnPersonCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
  const result: Tx[] = []
  for (const tx of _txes) {
    const ctx = tx as TxCreateDoc<Person>
    const userProfileTx = control.txFactory.createTxCreateDoc<UserProfile>(
      contact.class.UserProfile,
      card.space.Default,
      {
        person: ctx.objectId,
        title: formatName(ctx.attributes.name),
        rank: makeRank(undefined, undefined),
        content: '' as MarkupBlobRef,
        parentInfo: [],
        blobs: {}
      }
    )

    result.push(userProfileTx)
    result.push(
      control.txFactory.createTxUpdateDoc<Person>(ctx.objectClass, ctx.objectSpace, ctx.objectId, {
        profile: userProfileTx.objectId
      })
    )
  }
  return result
}

async function createPersonSpace (
  account: AccountUuid,
  person: Ref<Person>,
  control: TriggerControl
): Promise<TxCUD<PersonSpace>[]> {
  const personSpace = (await control.findAll(control.ctx, contact.class.PersonSpace, { person }, { limit: 1 }))[0]
  if (personSpace !== undefined) return []

  return [
    control.txFactory.createTxCreateDoc(contact.class.PersonSpace, core.space.Space, {
      name: 'Personal space',
      description: '',
      private: true,
      archived: false,
      person,
      members: [account]
    })
  ]
}

/**
 * @public
 */
export async function OnContactDelete (
  txes: Tx[],
  { findAll, hierarchy, storageAdapter, workspace, removedMap, txFactory, ctx }: TriggerControl
): Promise<Tx[]> {
  const result: Tx[] = []
  for (const tx of txes) {
    const rmTx = tx as TxRemoveDoc<Contact>

    const removeContact = removedMap.get(rmTx.objectId) as Contact
    if (removeContact === undefined) {
      continue
    }

    const members = await findAll(ctx, contact.class.Member, { contact: removeContact._id })
    for (const member of members) {
      const removeTx = txFactory.createTxRemoveDoc(member._class, member.space, member._id)
      const tx = txFactory.createTxCollectionCUD(
        member.attachedToClass,
        member.attachedTo,
        member.space,
        member.collection,
        removeTx
      )
      result.push(tx)
    }
  }

  return result
}

/**
 * @public
 */
export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
  const result: Tx[] = []

  for (const tx of txes) {
    const uTx = tx as TxUpdateDoc<Channel>

    if (uTx.operations.$inc?.items !== undefined) {
      const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0]
      const account = await getAccountBySocialId(control, tx.modifiedBy)
      if (doc !== undefined && account != null) {
        const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, doc.space, {
          attachedTo: doc._id,
          attachedToClass: doc._class,
          collection: 'collaborators',
          collaborator: account
        })
        result.push(tx)
      }
    }
  }

  return result
}

/**
 * @public
 */
export async function personHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
  const person = doc as Person
  const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
  const path = `${workbenchId}/${control.workspace.url}/${contactId}/${doc._id}`
  const link = concatLink(front, path)
  return `<a href="${link}">${getName(control.hierarchy, person, control.branding?.lastNameFirst)}</a>`
}

/**
 * @public
 */
export function personTextPresenter (doc: Doc, control: TriggerControl): string {
  const person = doc as Person
  return `${getName(control.hierarchy, person, control.branding?.lastNameFirst)}`
}

/**
 * @public
 */
export async function organizationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
  const organization = doc as Organization
  const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
  const path = `${workbenchId}/${control.workspace.url}/${contactId}/${doc._id}`
  const link = concatLink(front, path)
  return `<a href="${link}">${organization.name}</a>`
}

/**
 * @public
 */
export function organizationTextPresenter (doc: Doc): string {
  const organization = doc as Organization
  return `${organization.name}`
}

/**
 * @public
 */
export function contactNameProvider (
  doc: Doc,
  parent: Doc | undefined,
  space: Space | undefined,
  hierarchy: Hierarchy,
  mode: string
): string {
  if (parent !== undefined && hierarchy.isDerived(parent._class, contact.class.Contact)) {
    return formatContactName(hierarchy, parent._class, (parent as Contact).name ?? '', mode)
  }
  return formatContactName(hierarchy, doc._class, (doc as Contact).name ?? '', mode)
}

export async function getCurrentEmployeeName (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
  const person = await getCurrentPerson(control)

  return person !== undefined ? formatName(person.name, control.branding?.lastNameFirst) : ''
}

export async function getCurrentEmployeeEmail (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
  const person = await getCurrentPerson(control)
  if (person === undefined) return ''

  const emailSocialId = (
    await control.findAll(control.ctx, contact.class.SocialIdentity, {
      attachedTo: person._id,
      attachedToClass: contact.class.Person,
      type: SocialIdType.EMAIL
    })
  )[0]
  if (emailSocialId === undefined) return ''

  return emailSocialId.value
}

export async function getCurrentEmployeePosition (
  control: TriggerControl,
  context: Record<string, Doc>
): Promise<string | undefined> {
  const person = await getCurrentPerson(control)
  if (person === undefined) return ''

  return control.hierarchy.as(person, contact.mixin.Employee)?.position ?? ''
}

export async function getContactName (
  control: TriggerControl,
  context: Record<string, Doc>
): Promise<string | undefined> {
  const value = context[contact.class.Contact] as Contact
  if (value === undefined) return
  if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
    return getName(control.hierarchy, value, control.branding?.lastNameFirst)
  } else {
    return value.name
  }
}

export async function getContactLastName (
  control: TriggerControl,
  context: Record<string, Doc>
): Promise<string | undefined> {
  const value = context[contact.class.Contact] as Contact
  if (value === undefined) return
  if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
    return getLastName(value.name)
  } else {
    return ''
  }
}

export async function getContactFirstName (
  control: TriggerControl,
  context: Record<string, Doc>
): Promise<string | undefined> {
  const value = context[contact.class.Contact] as Contact
  if (value === undefined) return
  if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
    return getFirstName(value.name)
  } else {
    return value.name
  }
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
  trigger: {
    OnEmployeeCreate,
    OnTypedSpaceCreate,
    OnPersonCreate,
    OnContactDelete,
    OnChannelUpdate,
    OnSpaceTypeMembers
  },
  function: {
    PersonHTMLPresenter: personHTMLPresenter,
    PersonTextPresenter: personTextPresenter,
    OrganizationHTMLPresenter: organizationHTMLPresenter,
    OrganizationTextPresenter: organizationTextPresenter,
    ContactNameProvider: contactNameProvider,
    GetCurrentEmployeeName: getCurrentEmployeeName,
    GetCurrentEmployeeEmail: getCurrentEmployeeEmail,
    GetContactName: getContactName,
    GetCurrentEmployeePosition: getCurrentEmployeePosition,
    GetContactFirstName: getContactFirstName,
    GetContactLastName: getContactLastName
  }
})
